為了讓 Pod 有一個「統一、穩定」的存取介面,因此我們建立了 Service。
建立 Service 後,就可以透過 ClusterIP、NodePort、LoadBalancer 等方式來存取 Pod 。以下為一個簡單的例子:
目前總共有兩個 Deployments,負責提供圖片或影片讓使用者觀賞:
為了讓使用者能夠存取,並希望每個 Deployment 的 Pod 達到附載平衡,因此我們在每個 Deployment 上都配置了一個 loadBalancer 的 service:
假如使用者想要「看 dog 的圖片」,情況如下圖:
這樣的配置有以下缺點:
造成使用者的不便:
如上圖所示,使用者需要知道 service 的 Port 才能存取服務。如果未來 service 的數量增加,使用者就得記住更多的 Port。
Port 管理不便
如果 service 的數量增加,一堆的 Port number 會讓管理變得複雜。
Load balancer成本問題:
為了流量附載,通常會在 service 前面加上一個 load balancer,而 load balancer 是需要花錢的,如果 service 的數量增加,成本也會增加。
要改善上面的缺點,我們可以使用「Ingress」。
以下為一張官網上的 Ingress 圖示:
上面的圖示中,我們可以看到 Ingress 的功能有以下幾點:
Routing Rules
在 Ingress 的設定中將 URL path 與 service 對應起來,並統一開啟 http 的 80 port 或 https 的 443 port,這樣使用者就不需要記住 service 的 Port number,只需要知道 URL path 就可以存取服務。
統一的 Load balancer
Ingress 有提供 load balancer 的功能,可以掛在 service 的前面,這樣就不需要為每個 service 都配置一個 load balancer,可以節省成本。
統一管理 TLS
如果想讓 service 與使用者之間的通訊走 https,在沒有 Ingress 的情況下就需要每個 service 都配置TLS 憑證,這樣管理起來會很麻煩。
而 Ingress 有支援 TLS,不管 Ingress 背後有多少 service,只需要在 Ingress 上配置 TLS 憑證,就可以讓使用者透過 https 存取服務。
不過,上面講的 Ingress 設定沒有 Ingress controller 是無法生效的,因為 Ingress controller 才是這些設定的真正執行者。
Ingress controller 有很多種,每種各有其特色與功能,可以參考官方文件的列表進行選擇。
在一個 cluster 中可以同時存在多種 Ingress controller,每種 Ingress controller 都有所屬的 Ingress class 與自己的 service,兩者的功能如下:
Ingress class
Ingress class 的主要功能就是區隔不同的 Ingress 資源。
前面提到,Ingress 需要 Ingress controller 才能生效,所以設定 Ingress 時需指定「Ingress class」,來說明要使用哪一個 Ingress controller。
當哪天想要換其他的 controller 來執行相同的路由規則時,只需修改 Ingress 的「Ingress class」即可。
簡單來說,「用 Ingress class 來分隔不同的 Ingress 資源」,有些類似 StorageClass 的概念,用來分隔不同的 Storage 資源。
Ingress controller 的 service
當 Ingress 透過 Ingress controller 生效,使用者存取 Ingress 背後提供的服務時,流程如下:
先經過 Ingress controller 的 service 抵達 Ingress controller
Ingress controller 查找相對應的 Ingress URL 規則
流量從 URL 規則來到正確的 service,最終到達提供服務的 Pod。
而為了達成 Ingress 所制定的規則,Ingress controller 會透過 kube-apiserver 來監聽 service 與 Pod 的變化,這樣才能根據 Ingress 的規則做流量轉發。
一下講了這麼多名詞,這裡一樣用「使用者想看圖片」的圖示來說明一下整個流程:
我們改良了一開始的例子,用 Ingress 設定了以下 URL path:
想看圖片:
/watch-photo
:watch-photo 的首頁
/watch-photo/<file-path>
:watch-photo 提供的圖片 (ex. /watch-photo/dog)
想看影片:
/watch-video
:watch-video 的首頁
/watch-video/<file-path>
:watch-video 提供的影片 (ex. /watch-video/movie)
(藍色的圓圈即 ingress class)
接著,我們來實際安裝 Ingress controller。
以下選用 Ingress-Nginx Controller 作為範例
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.2/deploy/static/provider/cloud/deploy.yaml
kubectl get pods --namespace=ingress-nginx -w
NAME READY STATUS RESTARTS AGE
ingress-nginx-admission-create-httbh 0/1 Completed 0 17m
ingress-nginx-admission-patch-ztmmr 0/1 Completed 1 17m
ingress-nginx-controller-7dcdbcff84-wl484 1/1 Running 0 17m
重點是 ingress-nginx-controller-xxx 這個 Pod 有跑起來就好。
kubectl edit ingressclass nginx
apiVersion:networking.k8s.io/v1
kind:IngressClass
metadata:
annotations:
ingressclass.kubernetes.io/is-default-class: "true" # 加入這個annotation
......
上面的操作是將「nginx」設定為預設的 Ingress class,之後若設定 ingress 時沒有指定「sepc.ingressClassName」,就會使用預設。
Tips:關於ingress-nginx-admission
上面安裝過程中,不知道有沒有留意到兩個 completed 的 Pod,他們的任務是建立 ingress-admission。
admission 是 nginx-ingress-controller 的一個 webhook 插件,每當有新的 ingress 被創建或更新時,會交由 admission 檢查 ingress 是否符合規則,整個流程如下:
ingress創建/更新 --> admission 檢查 --> 符合規則 --> controller 執行
其他關於 admission 與 ingress-nginx-controller 的運作原理,可參考官方文件
底下將透過實作來說明 Ingress 的設定方式,本次實作的目的如下:
嘗試實現上面例子中的「watch」業務(看圖片、看影片),不過我們先嘗試最簡單的 Ingress 設定,熟練後再設定較複雜的。
kubectl run user --image=nginx
kubectl run watch-photo --image=nginx --port=80
kubectl run watch-video --image=nginx --port=80
kubectl expose pod watch-photo --port=80 --name=watch-photo-svc
kubectl expose pod watch-video --port=80 --name=watch-video-svc
echo "Home page of watch-photo" > watch-photo.html
echo "Home page of watch-video" > watch-video.html
kubectl cp watch-photo.html watch-photo:/usr/share/nginx/html/index.html
kubectl cp watch-video.html watch-video:/usr/share/nginx/html/index.html
# ingress-watch.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-watch
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /photo
pathType: Prefix
backend:
service:
name: watch-photo-svc
port:
number: 80
- path: /video
pathType: Prefix
backend:
service:
name: watch-video-svc
port:
number: 80
kubectl apply -f ingress-watch.yaml
重要欄位說明
annotations:這裡加入了「nginx.ingress.kubernetes.io/rewrite-target:/」,作用稍後會說明。
spec.rules:設定了兩個 URL Path 規則:
pathType:有三種選項
你也可以透過 kubectl 來建立相同的 Ingress:
kubectl create ingress ingress-watch --rule='/photo*=watch-photo-svc:80' --rule='/video*=watch-video-svc:80' --annotation='nginx.ingress.kubernetes.io/rewrite-target=/'
kubectl describe ingress ingress-watch
Name: ingress-watch
Labels: <none>
Namespace: default
Address:
Ingress Class: nginx # 上面沒指定ingress class,所以使用預設
Default backend: <default>
Rules:
Host Path Backends
---- ---- --------
*
/photo watch-photo-svc:80 (192.168.1.8:80)
/video watch-video-svc:80 (192.168.1.9:80)
Annotations: nginx.ingress.kubernetes.io/rewrite-target:/
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 7s nginx-ingress-controller Scheduled for sync
在 Rules 的 Backends 欄位中,可以看到對應到的 Pod IP:
測試Ingress效果
export ingressIP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.spec.clusterIP}')
kubectl exec -it user -- curl http://${ingressIP}/photo && curl http://${ingressIP}/video
Home page of watch-photo
Home page of watch-video
kubectl logs svc/watch-video-svc | grep "GET"
192.168.1.6 - - [25/Apr/2024:09:52:19 +0000] "GET / HTTP/1.1" 200 25 "-" "curl/7.68.0" "192.168.0.0"
以上就是相當簡單的 Ingress 測試,等下我們再來試試不同的 Ingress 功能。不過在此之前,先來填坑:到底「nginx.ingress.kubernetes.io/rewrite-target:/」這個 annotation是什麼意思 ?
vim ingress-watch.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-nginx
# annotations:
# nginx.ingress.kubernetes.io/rewrite-target: /
......
kubectl apply -f ingress-watch.yaml
kubectl exec -it user -- curl http://${ingressIP}/photo && curl http://${ingressIP}/video
結果系統丟回來「404 Not Found」!
kubectl logs svc/watch-photo-svc | grep "error"
2024/04/25 11:54:12 [error] 28#28:*2 open() "/usr/share/nginx/html/photo" failed (2:No such file or directory), client:192.168.1.6, server:localhost, request:"GET /photo HTTP/1.1", host:"10.103.62.152"
上述的錯誤主要是因為找不到 /usr/share/nginx/html/photo 這個檔案 (No such file or directory)
在解釋為何傳回 404 之前,這裡先補充一下 URL 的結構:
[Protocol]://[host]/[file-path]
以我們的例子「http://localhost:30906/watch-photo」來說:
Protocol:傳輸協定,這裡走「http」
host:可以是主機的 IP 或 domain name,這裡是「localhost」(本機的 domain name ),最後加上「port 30906」
file-path:想要存取的檔案路徑,這裡是「watch-photo」
所以當我們沒有加上 rewrite-target 的 annotation 時,user 在 curl 中的 URL 會經過以下轉換:
原本的 URL | 經 ingress 轉到的 service |
---|---|
http://${ingressIP}/photo | http://watch-photo-svc:80/photo |
由於提供服務的 Pod 都基於 nginx,所以 Pod 會預設你想存取的檔案路徑位於 /usr/share/nginx/html/ 之下,所以它會去找 /usr/share/nginx/html/photo 這個檔案路徑,但這個檔案並不存在,所以才會丟出 404。
先暫時不要修改 ingress,我們來驗證一下上面的說法:
kubectl exec -it watch-photo -- mv /usr/share/nginx/html/index.html /usr/share/nginx/html/photo
kubectl exec -it user -- curl http://${ingressIP}/photo
Home page of watch-photo
將 index.html 改名為 photo 後,這樣 Pod 才找的到「/usr/share/nginx/html/photo」,才能丟出正確的回應。
不過這樣的設定方式並不正統,我們還是希望使用 index.html 作為預設首頁。
因此,我們希望 user 輸入 URL 中的 file-path 會被「改寫」成 "/",這樣當 Pod 收到流量時就會認為使用者並沒有指定任何路徑,所以預設會去找 /usr/share/nginx/html/index.html:
原本的 URL | 經 ingress 轉到的 service |
---|---|
http://${ingressIP}/photo | http://watch-photo-svc:80/ |
所以 nginx.ingress.kubernetes.io/rewrite-target:/ 的作用就是將 ingress yaml 中的 rule.paths.path 改寫成「/」,
我們將 ingress 重新加回 annotation,並且再次測試一下:
kubectl exec -it watch-photo -- mv /usr/share/nginx/html/photo /usr/share/nginx/html/index.html
vim ingress-watch.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-nginx
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
......
kubectl apply -f ingress-watch.yaml
kubectl exec -it user -- curl http://${ingressIP}/photo && curl http://${ingressIP}/video
Home page of watch-photo
Home page of watch-video
另外,ingress 規則中的 path 設定與 rewrite-target 可以搭配正規表達式( egular Expressionr),我們再來看一個實作:
以下新的 watch-photo 與 watch-video 的檔案路徑配置,新增了「首頁」讓使用者造訪:
watch-photo:
watch-video:
vim ingress-watch.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-watch
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /photo(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: watch-photo-svc
port:
number: 80
- path: /video(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: watch-video-svc
port:
number: 80
kubectl apply -f ingress-watch.yaml
解釋
如果不熟悉正規表達式(regex),可先參考這裡
我們以 /photo(/|$)(.*) 為例來解釋:
/watch-photo(/|$)(.*) 設定了 2 個 regex 群組:
在 rewrite-target 中的 /$2
,代表將 path 中的第 2 個 regex 群組的字元取出來,例如當 user 輸入:
原本的 URL | 經 ingress 轉到的 service |
---|---|
http://${ingressIP}/photo | http://watch-photo-svc:80/ |
原本的 URL | 經 ingress 轉到的 service |
---|---|
http://${ingressIP}/photo/dog | http://watch-photo-svc:80/dog |
這樣就能實現造訪首頁的功能,且不影響造訪內容。
echo "dog" > dog.txt
echo "movie" > movie.txt
kubectl cp dog.txt watch-photo:/usr/share/nginx/html/dog
kubectl cp movie.txt watch-video:/usr/share/nginx/html/movie
kubectl exec -it user -- curl http://${ingressIP}/photo && curl http://${ingressIP}/video
Home page of watch-photo
Home page of watch-video
kubectl exec -it user -- curl http://${ingressIP}/photo/dog && curl http://${ingressIP}/video/movie
dog
movie
這樣是否能比較清楚 rewirte-target 的作用呢?可以試著自己使用其他的 regex 來設定 ingress 的 path,看看效果如何。
不過剛剛實作的 ingress 設定還是有問題,例如:
以使用者的角度來說,ingress controller 的 service 就是提供服務的 host,但因為沒有設定 host name,所以使用者需要知道該 host 的 IP 才能存取服務,這樣的設計根本沒有解決在文章開頭提到的問題。
所以較為理想的情況是使用者用 DNS 就可以存取到 host,我們來嘗試模擬一下:
假設 host 的 domain name 為 watch-photo-n-video.com:
kubectl exec -it user -- echo "${ingressIP} watch-photo-n-video.com" >> /etc/hosts && curl http://watch-photo-n-video.com/photo
Home page of watch-photo
雖然這樣成功解決了 user 的不便,但是還有一個問題:
kubectl exec -it user -- echo "${ingressIP} just-photo-only.com" >> /etc/hosts && curl http://just-photo-only.com/photo
Home page of watch-photo
可以發現依然存取的到 watch-photo 的首頁,但兩個意義截然不同的 domain name 都能存取到相同服務,這樣的設計是不太合理的。
要修正這個問題,我們得在 ingress 中加入「host」的設定,告訴 ingress controller只有這個 domain name 才是我們的host:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-watch
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- host: "watch-photo-n-video.com" # 加入host欄位,設定domain name
http:
paths:
- path: /photo(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: watch-photo-svc
port:
number: 80
- path: /video(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: watch-video-svc
port:
number: 80
kubectl apply -f ingress-watch.yaml
部署 ingress 後,測試一下正確的 domain name:
kubectl exec -it user -- echo "${ingressIP} watch-photo-n-video.com" >> /etc/hosts && curl http://watch-photo-n-video.com/photo
Home page of watch-photo
再次測試錯誤的domain name:
kubectl exec -it user -- echo "${ingressIP} just-photo-only.com" >> /etc/hosts && curl http://just-photo-only.com/photo
404 Not Found
當使用者輸入一個不存在的 Path 時,會得到 404 Not Found,不過這樣的回應對使用者來說並無實際幫助,我們可以透過 ingress 的 default backend 來解決這個問題。
kubectl run default-backend --image=nginx --port=80
kubectl expose pod default-backend --port=80 --name=default-backend-svc
echo "Do you mean /photo or /video?" > default-backend.txt
kubectl cp default-backend.txt default-backend:/usr/share/nginx/html/index.html
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-watch
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
defaultBackend:
service:
name: default-backend-svc
port:
number: 80
rules:
......
kubectl apply -f ingress-watch.yaml
kubectl exec -it user -- echo "${ingressIP} watch-photo-n-video.com" >> /etc/hosts && curl http://watch-photo-n-video.com/watch
Do you mean /photo or /video?
提示使用者他們輸入的 Path 不存在,並且知道該輸入哪些 Path。
我們曾在 Day 18 介紹過 Canary Deployment,並挖了一個實作的坑,因為那時候我們還沒有學過 Ingress,不過今天礙於篇幅關係,實作的部分可以參考這裡。
Ingress 其實就是一個「路由器」,可以讓使用者透過 URL path 存取服務,並且可以透過 host name 來區分不同的服務。其中 rewrite-target、正規表達式、default backend 等功能都可以讓 Ingress 更加靈活。
參考資料